;*****************************************************************************************************
;+
; NAME:
;       IGL_Window
;
; PURPOSE:
;
;        This is the heart of the Interactive Graphics Library. It
;        implements a graphic window with move, pan, zoom and
;        annotation capabilities. It is coded to behave
;        like enhanced regular IDL windows and is meant to be used as a
;        replacement of them. The object makes extensive use of the
;        features offered by the Catalyst Library.
;
;
;
; AUTHORS:
;
;        RAPHAEL SADOUN
;        Graduate Student
;        Institut d'Astrophysique de Paris
;        98 bis boulevard Arago
;        75014 PARIS FRANCE
;        E-mail: raphael.sadoun@gmail.com
;
;
; SUPERCLASSES:
;
;       TOPLEVELBASE (Catalyst Library)
;
; SYNTAX:
;
;       window = Obj_New('IGL_Window')
;
; CLASS_STRUCTURE:
;
;   class = {IGL_Window, $
;            INHERITS TopLevelBase, $        ; This is a TOPLEVELBASE widget 
;            window_id:0L, $                            ; Window index
;            window_title:'', $                            ; Window title
;            xmin:0L, $                                      ; Minimum dimensions
;            ymin:0L, $                                      ; for the draw widget
;            sx:0L, $                                           ; Record X and Y coordinates
;            sy:0L, $                                           ; of last cursor position
;            selectedObject:obj_new(), $        ; Record selected object for interaction
;            propertyPanel:Obj_new(), $         ; the reference to the property editor panel
;            toolManager:obj_new(), $            ; The CATTOOLLIST Object used for undo/redo capabilities
;            buffer:obj_new(), $                        ; The PIXMAPWIDGET object used for buffered graphic output
;            lastSavedFile:'' $                          ; The full name (path+name) of the last file output
;           }   
;                
;
; MODIFICATION_HISTORY:
;
;       Written by: Raphael Sadoun, 24 April 2012
;
;-
;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::ADDGRAPHIC
;
; PURPOSE:
;
;       This method add graphic objects to the draw widget and the pixmap buffer.
;
; SYNTAX:
;
;       self -> AddGraphic, object
;
; ARGUMENTS:
;
;       OBJECT:      The graphic object to add to the window.
;
; KEYWORDS:
;
;      _EXTRA:        Any keyword accepted by the DRAWWIDGET::Add method.
;
;-
;*****************************************************************************************************
PRO IGL_Window::AddGraphic, object, _EXTRA=extra

  drawWidget = self -> Get('DRAWWIDGET')

  IF (obj_valid(drawWidget)) THEN BEGIN
     drawWidget -> Add, object, _EXTRA=extra
     drawWidget -> GetProperty, RefreshBuffer=buffer
     IF (obj_valid(buffer)) THEN buffer -> Add, object, _EXTRA=extra
  ENDIF

END

;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::CLEARWINDOW
;
; PURPOSE:
;
;       This method removes all graphics contained in the draw widget
;       and the pixmap buffer. It also resets the undo/redo history.
;
; SYNTAX:
;
;       self -> ControlPanel, baseObject
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;       NOREFRESH:  By default, the DRAWWINDOW method is called after
;                                   the graphics have been removed in order to
;                                   update window's content. Set this keyword
;                                   to prevent this step.
;
;-
;*****************************************************************************************************
PRO IGL_Window::ClearWindow, NOREFRESH=norefresh

  @cat_pro_error_handler

  drawWidget = self -> Get('DrawWidget')
  IF (obj_valid(drawWidget)) THEN BEGIN
     drawWidget -> Remove, /all
     drawWidget -> GetProperty, RefreshBuffer=buffer
     IF (obj_valid(buffer)) THEN buffer -> Remove, /all
     IF (NOT keyword_set(norefresh)) THEN self -> DrawWindow, /BUFFERED_OUTPUT
  ENDIF

  IF OBJ_VALID(self.toolManager) THEN BEGIN
     self.toolManager -> Remove, /all
     undoButton = self -> Get('UndoButton',/Recursive_Search)
     IF OBJ_VALID(undoButton) THEN undoButton -> SetProperty, sensitive=0
     redoButton = self -> Get('RedoButton',/Recursive_Search)
     IF OBJ_VALID(redoButton) THEN redoButton -> SetProperty, sensitive=0
  ENDIF

END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::CONTROLPANEL
;
; PURPOSE:
;
;       This method creates a control panel for the IGL_Window
;       object. 
;
; SYNTAX:
;
;       self -> ControlPanel, baseObject
;
; ARGUMENTS:
;
;       baseObject:    The object reference of a base widget for this control to
;                                be added to. If not supplied, the control panel will be in a
;                                self contained window (i.e., a TOPLEVELBASE object).
;
; KEYWORDS:
;
;       _EXTRA:       Any keywords appropriate for the CatControlPanel::INIT method.
;
;-
;*****************************************************************************************************
PRO IGL_Window::ControlPanel, baseObject, _EXTRA=extraKeywords

   @cat_pro_error_handler

   ;; Create a new control panel
   cp = OBJ_NEW('CatControlPanel',self,PARENT=baseObject,COLUMN=1, $
                TITLE='Window Control Panel',_EXTRA=extraKeywords, /No_Cancel, /No_Apply, /No_OK)

   IF (NOT OBJ_VALID (cp)) THEN RETURN

   aproperties = Obj_New('PROPERTYSHEETWIDGET',cp,Value=self,Name='WINDOW PROPERTYSHEET',YSize=7)
   aproperties -> SetProperty, Event_Object=self

   ; Display the control panel if it created its own TLB.
   IF cp -> Created_Own_TLB(tlb) THEN tlb -> Draw, /Center
   
   self -> Report, /Completed

END


;*****************************************************************************************************
;+
; NAME:
;        IGL_Window::DRAWWIDGETEVENTHANDLER
;
; PURPOSE:
;
;        This method is the event handler for the draw widget 
;        contained in the window. This is where most of the
;        interactive capabilities are implemented.
;
; SYNTAX:
;
;        This method is called automatically by the event handling mechanism.
;
; ARGUMENTS:
;
;       event:    The event structure as described in the IDL help files, except
;                      that the ID, TOP and HANDLER tags will be object references.
;
; KEYWORDS:
;
;       None.
;
;-
;*****************************************************************************************************
PRO IGL_Window::DrawWidgetEventHandler, event

  @cat_pro_error_handler

  ;; If this is a tracking event, we only need to change the cursor
  ;; shape depending on which interaction is currently active
  IF (event.event_name EQ 'WIDGET_TRACKING') THEN BEGIN

     ;; Save current device and window
     entryDevice = !D.name
     entryWindow = !D.window
     
     ;; Switch to window device
     IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
     IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

     ;; Make the draw widget the current idl window
     event.id -> SetWindow
     
     IF (event.enter EQ 1) THEN BEGIN ; If the cursor is entering the window, gives it the appropriate shape
        
        CASE self -> GetCurrentInteraction() OF
           'MOVE': IGL_SetCursorShape, 'ARROW'
           'PAN':  IGL_SetCursorShape, 'CROSSARROWS'
           'ZOOM': IGL_SetCursorShape, 'CROSSHAIR'
           'ADDTEXT': IGL_SetCursorShape, 'VRULE'
           ELSE: IGL_SetCursorShape, 'ARROW'
        ENDCASE
        
     ENDIF ELSE IGL_SetCursorShape, 'CROSSHAIR' ; else restores the default IDL cursor

     ;; Restore device and window
     IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
     set_plot, entryDevice
     
     RETURN                    ; nothing more to do in this case
     
  ENDIF

  ;; Which type of events is it?
  eventTypes = ['PRESS','RELEASE','MOTION','VIEWPORT','EXPOSED','CHARACTER','KEYPRESS']
  thisEvent = eventTypes[event.type]

  CASE thisEvent OF
     
     ;; If a character is pressed, that means keyboard_events are
     ;; turned ON => We are typing a text in the window 
     'CHARACTER': BEGIN
        
        IF (obj_isa_valid(self.selectedObject,'IGL_Text') EQ 0) THEN return ; Need a valid text object
        
        IF (self -> GetCurrentInteraction() NE 'ADDTEXT') THEN return ; Only the ADDTEXT interaction can produce keyboard_events

        IF (event.press EQ 0) THEN return ; Process character typed only once (when the key is released)

        ;; The following part of code is inspired from the
        ;; TEXTLINE::InteractionEvents method of the CATALYST library
        CASE event.ch OF
           
           ;; Is this a Carriage Return? If so, process the text.
           13: BEGIN

              ;; Remove insertion mark.
              self.selectedObject -> GetProperty, text=newText
              newText = StrMid(newText, 0, StrLen(newText)-1)

              IF (newText EQ '') THEN BEGIN ; If the text is empty, destroy the text object and stop
                 obj_destroy, self.selectedObject
                 self -> DrawWindow, /BUFFERED_OUTPUT ; Redraw window's content to erase any remaining typed characters
                 RETURN
              ENDIF ELSE BEGIN  ; If the text is not empty, update the text object
                 self.selectedObject -> SetProperty, text=newText, /norefresh
                 IF OBJ_VALID(self.propertyPanel) THEN self.propertyPanel -> UpdateTreeBrowser
              ENDELSE

              IF (obj_valid(self.toolmanager)) THEN BEGIN
                 
                 ;; Create a text tool to undo/redo the text insertion
                 textTool = obj_new('IGL_TextTool',target=self.selectedObject)
                 textTool -> Cache, self ; a textTool needs the window reference
                 
                 ;; Add the tool to the list 
                 self.toolManager -> Add, textTool

                 ;; Activate undo button
                 undoButton = self -> Get('UNDOBUTTON',/recursive_search)
                 IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, sensitive=1

                 ;; Deactivate redo button
                 redoButton = self -> Get('REDOBUTTON',/recursive_search)
                 IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, sensitive=0

                 ;; Turn off keyboard_events and switch on button
                 ;; events for further interaction
                 event.id -> SetProperty, keyboard_events=0, button_events=1
                 
              ENDIF

              self.selectedObject = obj_new()
              
           END

           ;; Is this a backspace?
           8: BEGIN

              ;; Update the text object
              self.selectedObject -> GetProperty, text=newText
              IF (STRMID (newText, 0, /Reverse)) EQ '!' THEN rem = 3 ELSE rem = 2
              newText = STRMID (newText, 0, STRLEN (newText) - rem) + "|"
              self.selectedObject -> SetProperty, text=newText, /norefresh
              
           END
           
           ELSE: BEGIN

              ;; Special processing of exclamation marks in Hershey fonts.
              self.selectedObject -> GetProperty, text=newText
              IF (event.ch EQ 33) THEN newText = StrMid(newText,0, StrLen(newText)-1) $
                                                 + '!' + StrMid(newText,StrLen(newText)-1,1)

              ;; Update the text object
              newText = StrMid(newText,0, StrLen(newText)-1) + String(event.ch) + $
                        StrMid(newText,StrLen(newText)-1,1)
              self.selectedObject -> SetProperty, text=newText, /norefresh
              
           END
           
        ENDCASE                 ; End of case (event.ch)

        ;; At this point, the text object is updated and we need to
        ;; refresh window's content
        self -> DrawWindow, /Buffered_Output

        
     END                        ; End of event.type = CHARACTER

     
     'PRESS': BEGIN             ; This is a "click" event

        ;; Initialize the relevant variables and objects based on the
        ;; current interaction
        
        CASE self -> GetCurrentInteraction() OF

           'MOVE': BEGIN

              ;; Select the target object which is to be moved (only
              ;; one target is retained)
              targets = self -> SelectObjects(event.x,event.y,count=count) 
              IF (count EQ 0) THEN return
              targetObject = (reverse(targets))[0]

              ;; If this is a valid CatDataAtom, we can move it
              IF (obj_isa_valid(targetObject,'CatDataAtom')) THEN BEGIN
                 
                 self.selectedObject = targetObject ; Save the target object

                 ;; Save the cursor position
                 self.sx = event.x 
                 self.sy = event.y

                 IF (obj_valid(self.toolManager)) THEN BEGIN
                    
                    ;; Initialize a tool to undo/redo the move operation                   
                    tool = obj_new('IGL_MoveTool',target=targetObject)
                    tool -> CachePosition ; Save initial position
                    
                    ;; Add it to the tool list
                    self.toolManager -> Add, tool                    
                 
                 ENDIF

                 event.id -> SetProperty, /motion_events ; Turn ON motion events

              ENDIF
              
           END

           'PAN': BEGIN

              ;; Select the target object which is to be panned (only
              ;; one target is retained)
              targets = self -> SelectObjects(event.x,event.y,count=count) 
              IF (count EQ 0) THEN return
              targetObject = (reverse(targets))[0]

              ;; If this is a valid IGL_Graphic or an IGL_IMAGE, we can pan it
              IF obj_isa_valid(targetObject,'IGL_Graphic') THEN BEGIN
                 
                 self.selectedObject = targetObject ; Save the target object

                 ;; Save the cursor position
                 self.sx = event.x 
                 self.sy = event.y
                 
                 IF (obj_valid(self.toolManager)) THEN BEGIN
                    
                    ;; Initialize a tool to undo/redo the pan operation                   
                    tool = obj_new('IGL_RangeTool',target=targetObject)
                    tool -> CacheRanges ; Save initial x and y ranges
                    
                    ;; Add it to the tool list
                    self.toolManager -> Add, tool                    
                
                 ENDIF
                
                 event.id -> SetProperty,  /motion_events ; Turn ON motion events

              ENDIF

           END

           'ZOOM': BEGIN

              ;; Select the target object which is to be zoomed (only
              ;; one target is retained)
              targets = self -> SelectObjects(event.x,event.y,count=count) 
              IF (count EQ 0) THEN return
              targetObject = (reverse(targets))[0]

              ;; If this is a valid IGL_Graphic or IGL_Image, we can zoom it
              IF obj_isa_valid(targetObject,'IGL_Graphic') THEN BEGIN

                 ;; Save current device and window
                 entryDevice = !d.name
                 entryWindow = !d.window

                 ;; Switch to window device
                 IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
                 IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

                 ;; Make the draw widget the current IDL window                 
                 event.id -> SetWindow

                 IF (obj_valid(self.toolManager)) THEN BEGIN
                    
                    ;; Create a tool to undo/redo the zoom operation
                    tool = obj_new('IGL_Rangetool',target=targetObject)                    
                    tool -> CacheRanges ; Save initial x and y ranges
                    
                 ENDIF

                 ;; Zoom in if user did a left click
                 ;; Zoom out if used did a right click
                 IF (event.press EQ 1) THEN targetObject -> ZoomIn, event.x, event.y, /nodraw
                 IF (event.press EQ 4) THEN targetObject -> ZoomOut, event.x, event.y, /nodraw

                 self -> DrawWindow, /Buffered_Output

                 IF (obj_valid(self.toolManager)) THEN BEGIN

                    ;; Check if the zoom actually changed the ranges
                    changed = tool -> CompareCacheData()

                    IF (changed EQ 1) THEN BEGIN
                    
                       ;; Add the tool to the list
                       self.toolManager -> Add, Tool
                       
                       ;; Activate undo button
                       undoButton = self -> Get('UNDOBUTTON',/recursive_search)
                       IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, sensitive=1
                       
                       ;; Deactivate redo button
                       redoButton = self -> Get('REDOBUTTON',/recursive_search)
                       IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, sensitive=0

                    ENDIF
                    
                 ENDIF

                 ;; Restore device and window
                 IF (WindowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
                 set_plot, entryDevice
                 
              ENDIF
                 
           END
           
           ELSE:

        ENDCASE                 ; End of case self -> GetCurrentInteraction()
        
     END ; End of PRESS case

     
     'MOTION': BEGIN ; This an event generated by moving the cursor inside the window

        ;; If we don't have a valid selected object, nothing to do
        IF (obj_valid(self.selectedObject) EQ 0) THEN return

        ;; Save device and window
        entryDevice = !d.name
        entryWindow = !d.window

        ;; Switch to window device
        IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
        IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

        ;; Make this window the current IDL window to have correct
        ;; conversion from device to normal coordinates
        event.id -> SetWindow

        ;; Distances in X and Y direction from previous cursor position
        delta_x = event.x-self.sx
        delta_y = event.y-self.sy

        CASE self -> GetCurrentInteraction() OF

           'MOVE': BEGIN 

              ;; Move the target but don't refresh the window yet!
              self.selectedObject -> Move, delta_x, delta_y, /nodraw
              self -> DrawWindow, /Buffered_Output
              
           END

           'PAN': BEGIN
              
              ;; Pan the target but don't refresh the window yet!
              self.selectedObject -> Pan, delta_x, delta_y, /nodraw

              self -> DrawWindow, /Buffered_Output
              
           END

           ELSE:

        ENDCASE ; End of case self -> GetCurrentInteraction()

        ;; Save current cursor position
        self.sx = event.x
        self.sy = event.y

        ;; Restore device and window
        IF (WindowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
        set_plot, entryDevice
        
     END ; End of MOTION case

     
     'RELEASE': BEGIN ; This is a click release

        CASE self -> GetCurrentInteraction() OF

           'MOVE': BEGIN

              ;; If we don't have a valid selected object, nothing to do
              IF (obj_valid(self.selectedObject) EQ 0) THEN return

              ;; We don't need the reference of the selected object anymore
              self.selectedObject = obj_new()

              ;; Do not follow the cursor motion anylonger
              event.id -> SetProperty, motion_events=0

              IF (obj_valid(self.toolManager)) THEN BEGIN
                 
                 ;; Check if the move operation actually really moved the
                 ;; graphic. If not, don't bother keeping the tool for undo/redo.              
                 tool = self.toolManager -> Get(Count=count)
                 IF (count EQ 0) THEN RETURN
                 IF OBJ_ISA_VALID(tool,'IGL_MoveTool') EQ 0 THEN RETURN
                 IF (tool -> CompareCacheData() EQ 0) THEN self.toolManager -> Remove $
                 ELSE BEGIN
                    
                    ;; If the move operation was successful, activate the
                    ;; undo button and deactivate the redo button
                    undoButton = self -> Get('UNDOBUTTON',/recursive_search)
                    IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, sensitive=1
                    redoButton = self -> Get('REDOBUTTON',/recursive_search)
                    IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, sensitive=0
                    
                 ENDELSE

              ENDIF
              
           END

           'PAN': BEGIN

              ;; If we don't have a valid selected object, nothing to do
              IF (obj_valid(self.selectedObject) EQ 0) THEN return

              ;; We don't need the reference of the selected object anymore
              self.selectedObject = obj_new()

               ;; Do not follow the cursor motion anylonger
              event.id -> SetProperty, motion_events=0

              IF (obj_valid(self.toolManager)) THEN BEGIN
                 
                 ;; Check if the pan operation actually really panned the
                 ;; graphic. If not, don't bother keeping the tool for undo/redo.              
                 tool = self.toolManager -> Get(Count=count)
                 IF (count EQ 0) THEN RETURN
                 IF OBJ_ISA_VALID(tool,'IGL_RangeTool') EQ 0 THEN RETURN
                 IF (tool -> CompareCacheData() EQ 0) THEN self.toolManager -> Remove $
                 ELSE BEGIN
                    
                    ;; If the pan operation was successful, activate the
                    ;; undo button and deactivate the redo button
                    undoButton = self -> Get('UNDOBUTTON',/recursive_search)
                    IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, sensitive=1
                    redoButton = self -> Get('REDOBUTTON',/recursive_search)
                    IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, sensitive=0
                    
                 ENDELSE

              ENDIF

              self.selectedObject = obj_new()

           END

            'ADDTEXT':  BEGIN

              ;; Create a text object at the position where the user clicked
               event.id -> GetProperty, xsize=xsize, ysize=ysize
               self.selectedObject = obj_new('IGL_TEXT',"|",  $
                                             color=!IGL_P.color,  $
                                             x=event.x/float(xsize), $
                                             y=event.y/float(ysize), $
                                             font=0)

               ;; Add the text object to the window
               self -> AddGraphic, self.selectedObject

              ;; Refresh window's content              
              self -> DrawWindow, /Buffered_Output
              
              event.id -> SetProperty, keyboard_events=1, button_events=0, motion_events=0
              
           END

           ELSE:

        ENDCASE ; End of case self -> GetCurrentInteraction()
        
     END ; End of RELEASE case
     
     ELSE:
     
  ENDCASE ; end of case (event.type)

  ;; Report completion
  IF (obj_valid(self)) THEN self -> Report, /Completed
  
END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::DrawWindow
;
; PURPOSE:
;
;       This method draws the graphics inside the window by calling the
;       DRAW or the REFRESH (if BUFFERED_OUTPUT is set) method of
;       the draw widget. It takes care of setting up the correct plotting environment
;       before drawing to the window and restores previous values afterwards.
;
; SYNTAX:
;
;       self -> DrawWindow
;
; ARGUMENTS:
;
;
; KEYWORDS:
;
;       BUFFERED_OUTPUT:  Set this keyword to have buffered graphic output
;                                                  in the window. The graphics will first be drawn
;                                                  to the pixmap buffer and the buffer's content will be copied
;                                                  into the window. If there is no valid buffer available, a
;                                                  regular draw operation is done.
;
;       _EXTRA:                          Any keyword accepted by the DRAWWIDGET::Draw method.
;
;-
;*****************************************************************************************************
PRO IGL_Window::DrawWindow, BUFFERED_OUTPUT=buffered_output,  _EXTRA=extra

  @cat_pro_error_handler

  ;; Save current device and window 
  entryDevice = !d.name
  entryWindow = !d.window

  ;; Switch to window device
  IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
  IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

  ;; Save current color table
  TVLCT, r, g, b, /GET

  ;; Switch to decomposed color
  SetDecomposedState, 1, CURRENTSTATE=currentState

  ;; Draw the graphics
  drawWidget = self -> Get('DrawWidget')
  IF (obj_valid(drawWidget)) THEN BEGIN

     drawWidget -> ApplyColors
     IF (keyword_set(buffered_output)) THEN drawWidget -> Refresh ELSE drawWidget -> Draw, _EXTRA=extra

  ENDIF

  ;; Restore the decomposed state
  SetDecomposedState, currentState

  ;; Restore the color table
  TVLCT, r, g, b

  ;; Restore device and window
  IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
  set_plot, entryDevice
  
END



;*****************************************************************************************************
;+
; NAME:
;        IGL_Window::EVENT_HANDLER
;
; PURPOSE:
;
;        This method is the event handler for the IGL_Window
;        object. It responds to events generated by the widgets
;        in the window (buttons,...). Note that events generated by
;        the draw widget are handled by the DrawWidgetEventHandler method.
;
; SYNTAX:
;
;        This method is called automatically by the event handling mechanism.
;
; ARGUMENTS:
;
;       event: The event structure as described in the IDL help files, except
;              that the ID, TOP and HANDLER tags will be object references.
;
; KEYWORDS:
;
;       None.
;
;-
;*****************************************************************************************************
PRO IGL_Window::EventHandler, event

   @cat_pro_error_handler

   ; Branch on the object name.
   CASE StrUpCase(event.name) OF

      ;; These buttons start the corresponding interaction in the
      ;; window. We need to turn on button events for the draw widget.      
      'PANBUTTON' : BEGIN
         drawWidget = self -> Get('DrawWidget')
         IF (obj_valid(drawWidget)) THEN drawWidget -> SetProperty, /button_events
      END

      'MOVEBUTTON' : BEGIN
         drawWidget = self -> Get('DrawWidget')
         IF (obj_valid(drawWidget)) THEN drawWidget -> SetProperty, /button_events
      END

      'ZOOMBUTTON' : BEGIN
         drawWidget = self -> Get('DrawWidget')
         IF (obj_valid(drawWidget)) THEN drawWidget -> SetProperty, /button_events
      END

      'TEXTBUTTON': BEGIN
         drawWidget = self -> Get('DrawWidget')
         IF (obj_valid(drawWidget)) THEN drawWidget -> SetProperty, /button_events
      END

      ;; Automatically scale the possible graphics in the window
      'AUTOSCALEBUTTON': BEGIN

         ;; Save device and window
        entryDevice = !d.name
        entryWindow = !d.window

        ;; Switch to window device
        IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
        IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

        ;; Get the draw widget object reference
        drawWidget = self -> Get('DrawWidget')

        ;; Make sure we have a valid object
        IF OBJ_VALID(drawWidget) EQ 0 THEN RETURN

        ;; Make this window the current IDL window
        drawWidget -> SetWindow

        ;; Assume no graphics will be autoscaled
        status = 0

        ;; Auto-scale all the objects which can be rescaled and save
        ;; each auto scale operation in a tool for later undo/redo.
         FOR i=0, drawWidget -> Count()-1 DO BEGIN
            graphic = drawWidget -> Get(position=i)
            IF obj_isa_valid(graphic,'IGL_Graphic') THEN BEGIN
               tool = obj_new('IGL_RangeTool',target=graphic)
               tool -> CacheRanges
               graphic -> ZoomOut, /alltheway, /nodraw
               changed = tool -> CompareCacheData()
               IF (changed EQ 1) THEN BEGIN
                  self.toolManager -> Add, tool
                  status = 1
               ENDIF
            ENDIF
         ENDFOR

         ;; Restore device and window
         IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
         set_plot, entryDevice
         
         ;; If one of the objects were indeed autoscaled, then we need
         ;; to redraw window's content and to activate undo button.
         IF (status EQ 1) THEN BEGIN
            
            self -> DrawWindow, /BUFFERED_OUTPUT
            
            undoButton = self -> Get('UNDOBUTTON',/recursive_search)
            IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, sensitive=1
            redoButton = self -> Get('REDOBUTTON',/recursive_search)
            IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, sensitive=0
            
        ENDIF

      END

      ;; Pops up the property editor panel for the window and its content.
      'EDITBUTTON': BEGIN
         
         IF (obj_valid(self.propertyPanel)) THEN obj_destroy, self.propertyPanel ; Only one panel can be opened at any given time

         ;; Save device and window
        entryDevice = !d.name
        entryWindow = !d.window

        ;; Switch to window device
        IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
        IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'
         
         ;; Create an IGL_PropertyEditor (derived from
         ;; PROPERTYPANEL class). Only considers objects that can be
         ;; rendered (CatDataAtom).
         propertyPanel = obj_new('IGL_PropertyEditor',self, $
                                 map=0, $
                                 /floating, $
                                 group_leader=self, $
                                 filter='CatDataAtom')

         ;; Center the panel on the window and make it visible
         self -> GetProperty, geometry=window_geometry
         xoffset = window_geometry.xoffset+window_geometry.scr_xsize/2.0
         yoffset = window_geometry.yoffset+window_geometry.scr_ysize/2.0
         propertyPanel -> Position, xoffset, yoffset, /DEVICE
         propertyPanel -> SetProperty, /map

         ;; Restore device and window
         IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
         set_plot, entryDevice
         
         self.propertyPanel = propertyPanel ; Keep the object reference to check for number of instances
         
      END

      ;; Request an undo operation from the tool Manager (CATTOOLIST object)
      'UNDOBUTTON': BEGIN
         
         IF (NOT obj_valid(self.ToolManager)) THEN return ; Need a valid CATTOOLLIST object
         
         ;; Save current device and window
         entryDevice = !d.name
         entryWindow = !d.window

         ;; Switch to window device
         IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
         IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

         ;; Make the draw widget the current IDL window
         drawWidget = self -> Get('DrawWidget')
         drawWidget -> SetWindow

         ;; Undo the current tool
         self.ToolManager -> Undo

         ;; Update window's content
         self -> DrawWindow, /BUFFERED_OUTPUT
         
         ;; Activate the redo button
         redoButton = self -> Get('RedoButton',/recursive_search)
         IF (obj_valid(redoButton)) THEN redoButton -> SetProperty, /sensitive

         ;; If this was the first tool in the list, deactivate the undo button
         self.ToolManager -> GetProperty, current_index=current_index
         IF (current_index LT 0) THEN event.id -> SetProperty, sensitive=0

         ;; Restore device and window
         IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
         set_plot, entryDevice
         
      END

      ;; Request a redo operation from the tool Manager (CATTOOLIST object)
      'REDOBUTTON': BEGIN
         
         IF (NOT obj_valid(self.ToolManager)) THEN return ; Need a valid CATTOOLLIST object
         
         ;; Save current device and window
         entryDevice = !d.name
         entryWindow = !d.window

         ;; Switch to window device
         IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
         IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

         ;; Make the draw widget the current IDL window
         drawWidget = self -> Get('DrawWidget')
         drawWidget -> SetWindow

         ;; Redo the current tool
         self.ToolManager -> Redo

         ;; Update window's content
         self -> DrawWindow, /BUFFERED_OUTPUT
         
         ;; Activate the undo button
         undoButton = self -> Get('UndoButton',/recursive_search)
         IF (obj_valid(undoButton)) THEN undoButton -> SetProperty, /sensitive

         ;; If this was the last tool in the list, deactivate the redo
         ;; button
         self.ToolManager -> GetProperty, current_index=current_index
         nTools = self.ToolManager -> Count()
         IF (current_index GE nTools-1) THEN event.id -> SetProperty, sensitive=0

         ;; Restore device and window
         IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
         set_plot, entryDevice
         
      END

      'SAVEBUTTON': BEGIN

         ;; Create the dialog save object
         dialogWidget = obj_new('IGL_SaveFileWidget', $
                                initialFile=self.lastSavedFile, $
                                /modal, $
                                group_leader=self)

         ;; Get the geometry to center the dialog on top of this
         ;; window
         self -> GetProperty, geometry=win_geo

         ;; Calculate the offset for the save dialog
         xoffset = win_geo.xoffset+win_geo.scr_xsize/2.0
         yoffset = win_geo.yoffset+win_geo.scr_ysize/2.0

         ;; Position the dialog on top of the window
         dialogWidget -> Position, xoffset, yoffset, /DEVICE
                  
         ;; Register the window for SAVEFILE messages (The save button
         ;; is pushed)
         dialogWidget -> RegisterForMessage, self, 'IGL_SAVEFILE'

         ;; Display the save dialog
         dialogWidget -> Draw
         
      END
      
      ;; This means an event generated by the window itself -> Must be
      ;; a resize event.
      'WINDOW': BEGIN

         ;; Find new x and y sizes
         xsize = event.x > self.xmin 
         ysize = event.y > self.ymin

         ;; Resize the draw widget
         drawWidget = self -> Get("DrawWidget")
         IF (obj_valid(drawWidget)) THEN BEGIN
            drawWidget -> Resize, xsize, ysize
            drawWidget -> GetProperty, RefreshBuffer=buffer
            IF (obj_valid(buffer)) THEN buffer -> Resize, xsize, ysize, /NOREFRESH
         ENDIF

         ;; Redraw window's content
         self -> DrawWindow, /Buffered_Output
         
         ;; Update !IGL_D system variable if we are the current window
         currentWindow = IGL_GetSysVars('currentWindow')
         IF (currentWindow EQ self.window_id) THEN BEGIN
            !igl_d.x_size = xsize
            !igl_d.y_size = ysize
         ENDIF
         
      END

      ;; This means an event generated by the window's property sheet
      'WINDOW PROPERTYSHEET': BEGIN

         IF event.type EQ 0 THEN BEGIN ; Button DOWN even (i.e. a property was changed)

            CASE StrUpCase(event.identifier) OF

               'WINDOW_COLOR':BEGIN ; If we want to change the color, display the PickColorName dialog
                  event.component -> GetProperty, window_color=window_color
                  event.id -> GetProperty, ID=group_leader
                  window_color = PickColorName(window_color, Group_Leader=group_leader)
                  event.component -> SetProperty, window_color=window_color
                  self -> DrawWindow, /Buffered_Output
               END

               'COLORTABLE':BEGIN ; Here we want to change the internal color table

                  ;; Make sure the draw widget is valid
                  drawWidget = self -> Get('DRAWWIDGET')
                  IF OBJ_ISA_VALID(drawWidget,'DRAWWIDGET') EQ 0 THEN RETURN

                  ;; Make sure the color table object is valid
                  drawWidget -> GetProperty, Color_Object=colorTable
                  IF OBJ_ISA_VALID(colorTable,'IGL_ColorTable') EQ 0 THEN RETURN

                  ;; Get the window geometry to place the Xcolors
                  ;; dialog on top of it
                  self -> GetProperty, geometry=geo

                  ;; Calculate the offset for the Xcolors dialog
                  xoffset = geo.xoffset+geo.scr_xsize/2.0
                  yoffset = geo.yoffset+geo.scr_ysize/2.0

                  colorTable -> Xcolors, xoffset=xoffset, yoffset=yoffset, group_leader=self.PropertyPanel

               END
               
               ELSE:BEGIN
                  component = event.component
                  identifier = event.identifier
                  event.id -> GetProperty, Value=value, Component=component, Property_Value=identifier
                  event.component -> SetPropertyByIdentifier, identifier, value
                  self -> DrawWindow, /Buffered_Output
               END
               
            ENDCASE
            
         ENDIF
         
      END
      
      ELSE:

   ENDCASE

   ;; Report completion
   IF Obj_Valid(self) THEN self -> Report, /Completed

END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::GETCURRENTINTERACTION
;
; PURPOSE:
;
;       This method returns the name of the current active interaction
;       in the window based on which of the exclusive buttons is set.
;       It is used internally as a convenience method.
;
; SYNTAX:
;
;       currentInteraction = self -> GetCurrentInteraction()
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;       None.
;-
;*****************************************************************************************************
FUNCTION IGL_Window::GetCurrentInteraction

  @cat_func_error_handler

  button = self -> Get('MoveButton',/recursive_search)
  IF (obj_isa_valid(button,'ButtonWidget')) THEN BEGIN
     button -> GetProperty, button_set=MoveIsSet
     IF (MoveIsSet) THEN return, 'MOVE'
  ENDIF

  button = self -> Get('PanButton',/recursive_search)
  IF (obj_isa_valid(button,'ButtonWidget')) THEN BEGIN
     button -> GetProperty, button_set=PanIsSet
     IF (PanIsSet) THEN return, 'PAN'
  ENDIF

  button = self -> Get('ZoomButton',/recursive_search)
  IF (obj_isa_valid(button,'ButtonWidget')) THEN BEGIN
     button -> GetProperty, button_set=ZoomIsSet
     IF (ZoomIsSet) THEN return, 'ZOOM'
  ENDIF

  button = self -> Get('EditButton',/recursive_search)
  IF (obj_isa_valid(button,'ButtonWidget')) THEN BEGIN
     button -> GetProperty, button_set=EditIsSet
     IF (EditIsSet) THEN return, 'EDIT'
  ENDIF

  button = self -> Get('TextButton',/recursive_search)
  IF (obj_isa_valid(button,'ButtonWidget')) THEN BEGIN
     button -> GetProperty, button_set=EditIsSet
     IF (EditIsSet) THEN return, 'ADDTEXT'
  ENDIF

  return, ''
  
END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::GETPROPERTY
;
; PURPOSE:
;
;       This method is used to get the object's properties.
;
; SYNTAX:
;
;       self -> GetProperty
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;       PROPERTYPANEL:    The reference to the property editor associated with this window.
;
;       WINDOW_ID:               The window identifier used to reference the
;                                               window internally.
;
;       WINDOW_TITLE:         The title that appears at the top of the window. 
;
;       WINDOW_XSIZE:        The width of the window in device coordinates
;                                              (pixels). This corresponds to the xsize of the
;                                              draw widget.
;
;       WINDOW_YSIZE:        The height of the window in device coordinates
;                                              (pixels). This corresponds to the ysize of the
;                                              draw widget.
;
;       WINDOW_COLOR:     The name of the background color used when erasing
;                                               window's content.
;-
;*****************************************************************************************************
PRO IGL_Window::GetProperty, WINDOW_ID=window_id, $
                             WINDOW_TITLE=window_title, $
                             WINDOW_XSIZE=window_xsize, $
                             WINDOW_YSIZE=window_ysize, $
                             WINDOW_COLOR=window_color, $
                             PROPERTYPANEL=propertypanel, $
                             _Ref_Extra=extrakeywords

   @cat_pro_error_handler

   IF (arg_present(propertypanel)) THEN propertypanel = self.propertypanel
   IF (arg_present(window_id)) THEN window_id = self.window_id
   IF (arg_present(window_title)) THEN window_title = self.window_title
   drawWidget = self -> Get('DrawWidget')
   IF (obj_valid(drawWidget)) THEN drawWidget -> GetProperty, xsize=window_xsize, ysize=window_ysize, initial_color=window_color, COLOR_OBJECT=colorTool
   
   IF N_Elements(extrakeywords) NE 0 THEN self -> TopLevelBase::GetProperty, _Extra=extrakeywords

   self -> Report, /Completed

END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::MESSAGEHANDLER
;
; PURPOSE:
;
;       This method responds to "messages" sent from other objects. It is called
;       automatically by other objects. Typically, the window receives
;       messages when its internal color table is changed or when a
;       file is to be saved.
;
; SYNTAX:
;
;       None. Called by other objects.
;
; ARGUMENTS:
;
;       TITLE:  The message title. This is the "name" of the message indicated when
;               the object registered for messages with the messaging object.
;
; KEYWORDS:
;
;       DATA:   Information from the SENDER that may be relevant in processing the message.
;               Typically, an anonymous structure variable, although it could be anything at all.
;
;       SENDER: An output keyword. This is the object reference of the object that is sending
;               the message.
;-
;*****************************************************************************************************
PRO IGL_Window::MessageHandler, title, SENDER=sender, DATA=data

  @cat_pro_error_handler

  CASE strupcase(title) OF

     'COLORTOOL_TABLECHANGE': BEGIN

        ;; Get the CATCOLORS object associated with the window
        drawWidget = self -> Get('DRAWWIDGET')
        drawWidget -> GetProperty, Color_Object=colorTable

        ;; If the sender is not the image's colortool object, then update image colors.
        IF sender NE colorTable THEN $
           colorTool -> SetProperty, Red=data.r, Green=data.g, Blue=data.b, Bottom=data.bottom

        ;; Update control panel
        colorTable -> GetProperty, index=ctindex
        LOADCT, GET_NAMES=tableNames, /silent
        self -> SetPropertyAttribute, 'ColorTable', userdef=strlowcase(tableNames[ctindex])
        IF (obj_valid(self._controlpanel)) THEN self._controlpanel -> Refresh_Properties, Properties='ColorTable'

        ;; Refresh window's content
        self -> DrawWindow, /Buffered_Output
        
     END

     'IGL_SAVEFILE': BEGIN

        ;; Get the filename and the format of the file to be saved
        sender -> GetProperty, file=file, format=format

        ;; Save window's content to disk file
        self -> SaveWindow, file, format
        
        ;; Keep full name of save file to open the next save dialog at
        ;; the same location
        self.lastSavedFile = file
        
     END

     ELSE: self -> TopLevelBase::MessageHandler, title, SENDER=sender, DATA=data

  ENDCASE

END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::REMOVEGRAPHIC
;
; PURPOSE:
;
;       This method removes graphic objects from the draw widget and the pixmap buffer. They are no
;       longer displayed in the window when the next draw operation is performed.
;
; SYNTAX:
;
;       self -> RemoveGraphic, object
;
; ARGUMENTS:
;
;       OBJECT:      The object to remove from the window.
;
; KEYWORDS:
;
;      _EXTRA:       Any keyword accepted by the DRAWWIDGET::Remove method.
;
;-
;*****************************************************************************************************
PRO IGL_Window::RemoveGraphic, object, _EXTRA=extra

  drawWidget = self -> Get('DRAWWIDGET')

  IF (obj_valid(drawWidget)) THEN BEGIN
     drawWidget -> Remove, object, _EXTRA=extra
     drawWidget -> GetProperty, RefreshBuffer=buffer
     IF (obj_valid(buffer)) THEN buffer -> Remove, object, _EXTRA=extra
  ENDIF

END

;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::SELECTOBJECTS
;
; PURPOSE:
;
;       This method searches the object hierarchy for objects
;       located at given location in the window, starting at the draw
;       widget object. If no objects are found, it returns a NULL
;       object reference.
;
; SYNTAX:
;
;       selectedObjects = self -> SelectObject(x,y)
;
; ARGUMENTS:
;
;       X:      The X location of the selection in the draw widget window.
;       Y:      The Y location of the selection in the draw widget window.
;
; KEYWORDS:
;
;      COUNT:  Returns the number of valid objects found.
;
;-
;*****************************************************************************************************
FUNCTION IGL_Window::SelectObjects, x, y, COUNT=count

  @cat_func_error_handler

  ;; Assume no objects are selected
  retVal = Obj_New()
  count = 0

  ;; Get the reference of the drawWidget object
  drawWidget = self -> Get('DrawWidget')
  IF (NOT obj_valid(drawWidget)) THEN return, retVal

  ;; Save device settings and switch to window device 
  entryDevice = !d.name
  entryWindow = !d.window
  IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
  IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

  ;; Make the draw widget the current IDL window (this is necessary
  ;; because some objects need a valid IDL window when deciding if
  ;; they are selected or not)
  drawWidget -> SetWindow
  
  ;; Search for selected objects using the SelectObjects method of the
  ;; draw Widget
  retVal = drawWidget -> SelectObjects(x,y,count=count)

  ;; Restore device and window
  IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
  set_plot, entryDevice

  ;; Report completion
  self -> Report, /Completed

  return, retVal
  
END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::SETPROPERTY
;
; PURPOSE:
;
;       This method is used to set the object's properties.
;
; SYNTAX:
;
;       self -> SetProperty
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;      WINDOW_TITLE: The title that appears at the top of the window. 
;
;      WINDOW_XSIZE: The width of the window in device coordinates
;                    (pixels). This corresponds to the xsize of the
;                    draw widget.
;
;      WINDOW_YSIZE: The height of the window in device coordinates
;                    (pixels). This corresponds to the ysize of the
;                    draw widget.
;
;      WINDOW_COLOR: The name of the background color used when erasing
;                    window's content.
;
;-
;*****************************************************************************************************
PRO IGL_Window::SetProperty,  WINDOW_TITLE=window_title, $
                              WINDOW_XSIZE=window_xsize, $
                              WINDOW_YSIZE=window_ysize, $
                              WINDOW_COLOR=window_color, $
                              _Extra=extrakeywords

   @cat_pro_error_handler

   IF (n_elements(window_title) NE 0) THEN BEGIN
      self.window_title = window_title
      self -> TopLevelBase::SetProperty, title=window_title
   ENDIF
   drawWidget = self -> Get('DrawWidget')
   IF (obj_valid(drawWidget)) THEN BEGIN
      drawWidget -> SetProperty, xsize=window_xsize, ysize=window_ysize, initial_color=window_color
      drawWidget -> GetProperty, RefreshBuffer=buffer
      IF (obj_valid(buffer)) THEN buffer -> SetProperty, Xsize=window_xsize, Ysize=window_ysize, backgroundcolor=window_color
   ENDIF
   IF (n_elements(window_color) NE 0) THEN BEGIN
      self -> SetPropertyAttribute, 'Window_Color', userdef=CapFirstLetter(window_color)
      IF (obj_valid(self._controlpanel)) THEN self._controlpanel -> Refresh_Properties, Properties='Window_Color'
   ENDIF

   IF N_Elements(extrakeywords) NE 0 THEN self -> TopLevelBase::SetProperty, _Extra=extrakeywords

   ;; Report completion
   self -> Report, /Completed

END

;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::SAVEWINDOW
;
; PURPOSE:
;
;       This method is used to save window's content to disk file.
;
; SYNTAX:
;
;       self -> SetProperty
;
; ARGUMENTS:
;
;       FILENAME:  The full name (path+basename) of the desired output
;                  file. The default is 'graphic.ps'
;
;
;       FORMAT:    The format of the output file.
;                  Available formats are :
;
;                        ['ps','png','jpg','bmp','tiff']
;
;                  The default is to use the file extension if there
;                  is one. Otherwise, it is set to 'ps'.
;
; KEYWORDS:
;
;       _EXTRA:    Any keyword accepted by the PS_START routine.
;
;-
;*****************************************************************************************************
PRO IGL_Window::SaveWindow, filename, format, _EXTRA=extra

  @cat_pro_error_handler

  ;; Default filename
  IF (n_elements(filename) EQ 0) THEN filename = 'graphic.ps'

  ;; Get path and extension components of the filename
  basename = FSC_BASE_FILENAME(filename,directory=dir,extension=ext)

  ;; If no format is provided, use the file extension if possible
  ;; Otherwise, use 'ps'
  IF (n_elements(format) EQ 0) THEN IF (ext NE '') THEN format = ext ELSE format = 'ps'

  ;; Make sure we have a valid format
  formats = ['ps','png','jpg','bmp','tiff']
  index = where(strlowcase(format) EQ formats,count)
  IF (count EQ 0) THEN message, 'Invalid file format'

  ;; Make sure the draw widget is valid
  drawWidget = self -> Get('DRAWWIDGET')
  IF (OBJ_ISA_VALID(drawWidget,'DRAWWIDGET') EQ 0) THEN RETURN

  ;; Save device settings and switch to window device 
  entryDevice = !D.name
  entryWindow = !D.window
  IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
  IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

  ;; Save current color table
  TVLCT, r, g, b, /get

  ;; Switch to decomposed color
  SetDecomposedState, 1, CURRENTSTATE=currentState

  ;; Save the global variables
  vx = !IGL_X
  vy = !IGL_Y
  vz = !IGL_Z 

  ;; Make the draw widget the current window
  drawWidget -> SetWindow

  ;; Get the erase color of the drawWidget
  drawWidget -> GetProperty, INITIAL_COLOR=color

  ;; Check if there is a valid ImageMagick program
  validImageMagick  = HasImageMagick()
  
  CASE strupcase(format) OF

     'PS': BEGIN
        
        PS_START, /QUIET, FILENAME=filename, _EXTRA=extra
        POLYFILL, [0,0,1,1], [0,1,1,0], COLOR=cgColor(color), /NORMAL ; Needed to color the background in PostScript
        drawWidget -> draw
        PS_END
        
     END
           
     'PNG': BEGIN

        IF (validImageMagick EQ 1) THEN BEGIN

           PS_START, /QUIET, FILENAME=filename, _EXTRA=extra
           POLYFILL, [0,0,1,1], [0,1,1,0], COLOR=cgColor(color), /NORMAL ; Needed to color the background in PostScript
           drawWidget -> draw
           PS_END, /DELETE_PS, /PNG

        ENDIF ELSE BEGIN

           drawWidget -> Output, FILENAME=filename, /PNG, /NODIALOG

        ENDELSE                
        
     END
     
     'JPG': BEGIN

        IF (validImageMagick EQ 1) THEN BEGIN
           
           PS_START, /QUIET, FILENAME=filename, _EXTRA=extra
           POLYFILL, [0,0,1,1], [0,1,1,0], COLOR=cgColor(color), /NORMAL ; Needed to color the background in PostScript
           drawWidget -> draw
           PS_END, /DELETE_PS, /JPEG
           
        ENDIF ELSE BEGIN
           
           drawWidget -> Output, FILENAME=filename, /JPEG, /NODIALOG
           
        ENDELSE                
        
     END     
     
     'BMP': BEGIN

        IF (validImageMagick EQ 1) THEN BEGIN
           
           PS_START, /QUIET, FILENAME=filename, _EXTRA=extra
           POLYFILL, [0,0,1,1], [0,1,1,0], COLOR=cgColor(color), /NORMAL ; Needed to color the background in PostScript
           drawWidget -> draw
           PS_END, /DELETE_PS, /BMP
           
        ENDIF ELSE BEGIN
           
           drawWidget -> Output, FILENAME=filename, /BMP, /NODIALOG
           
        ENDELSE                
        
     END
     
     'TIFF': BEGIN

        IF (validImageMagick EQ 1) THEN BEGIN
           
           PS_START, /QUIET, FILENAME=filename, _EXTRA=extra
           POLYFILL, [0,0,1,1], [0,1,1,0], COLOR=cgColor(color), /NORMAL ; Needed to color the background in PostScript
           drawWidget -> draw
           PS_END, /DELETE_PS, /TIFF
           
        ENDIF ELSE BEGIN
           
           drawWidget -> Output, FILENAME=filename, /TIFF, /NODIALOG
           
        ENDELSE                
        
     END
     
     ELSE: message, 'Invalid file format'
     
  ENDCASE

  ;; Restore the global variables
  !IGL_X = vx
  !IGL_Y = vy
  !IGL_Z = vz

  ;; Restore decomposed state
  SetDecomposedState, currentState
  
  ;; Restore current color table
  TVLCT, r, g, b

  ;; Restore device and window
  IF (windowAvailable(entryWindow)) THEN wset, entryWindow ELSE wset, -1
  set_plot, entryDevice
  
  ;; Report completion
  self -> Report, /Completed

END

;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::CLEANUP
;
; PURPOSE:
;
;       This is the IGL_Window object class destructor method.
;
; SYNTAX:
;
;       Called automatically when the object is destroyed.
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;       None.
;-
;*****************************************************************************************************
PRO IGL_Window::Cleanup

   @cat_pro_error_handler

   ;; We don't need the CATTOOLLIST object anymore
   IF OBJ_VALID(self.toolManager) THEN self.toolManager -> RemoveParent, self

   ;; Destroy the pixmap buffer
   IF OBJ_VALID(self.buffer) THEN self.buffer -> RemoveParent, self
   
   ;; Update global system variables
   !igl_d.win_state[self.window_id] = 0 ; This window is no longer valid
   currentWindow = IGL_GetSysVars('currentWindow')
   windowObjRefs = IGL_GetSysVars('WindowObjRefs')
   IF (currentWindow EQ self.window_id) THEN BEGIN ; If this is the current window, we need to update the !IGL_D variable
      excludeIndex = where(indgen(50) NE self.window_id)
      targetWindows = windowObjRefs[excludeIndex]
      validIDs = where(obj_isa_valid(targetWindows,'IGL_Window'),count) ; Find the next available window
      IF (count GT 0) THEN BEGIN                                        ; If we found one, make it the current window
         newCurrentWindow = targetWindows[validIDs[count-1]]
         newCurrentWindow -> GetProperty, window_id=window_id, xsize=xsize, ysize=ysize
         !igl_d.window = window_id
         !igl_d.x_size = xsize
         !igl_d.y_size = ysize
         IGL_SetSysVars, 'currentWindow', window_id
      ENDIF ELSE BEGIN          ; if no other windows are available, reset the global variables
         !igl_d.window = -1L
         !igl_d.x_size = 640L
         !igl_d.y_size = 480L
         IGL_SetSysVars, 'currentWindow', -1L
     ENDELSE
   ENDIF

   ;; Call the CLEANUP method of the superclass
   self -> TopLevelBase::Cleanup

   ;; Report completion
   self -> Report, /Completed
   
END


;*****************************************************************************************************
;+
; NAME:
;       IGL_Window::INIT
;
; PURPOSE:
;
;       This is the IGL_Window object class creator method.
;
; SYNTAX:
;
;       Called automatically when the object is created.
;
; ARGUMENTS:
;
;       None.
;
; KEYWORDS:
;
;       WINDOW_ID:    The identifier of the window. This is the number
;                     that appears next to the "IDL" text by
;                     default. If not provided, will be set to the
;                     next available ID between 0 and 31. (input)
;
;       WINDOW_TITLE: The title that appears at the top of the
;                     window. The default is "IDL (ID)" where ID is
;                     the identifier of the window. (input)
;
;       WINDOW_XSIZE: The width of the window in device coordinates
;                     (pixels). This corresponds to the xsize of the
;                     draw widget. The default is 640. (input)
;
;       WINDOW_YSIZE: The height of the window in device coordinates
;                     (pixels). This corresponds to the ysize of the
;                     draw widget. The default is 480. (input)
;
;       WINDOW_COLOR: The name of the background color used when erasing
;                     window's content. The default is
;                     !IGL_P.background. (input)
;
;       XPOS:         The initial X position of the window in device
;                     coordinates. (input)
;
;       YPOS:         The initial Y position of the window in device
;                     coordinates. (input)
;
;       FREE:         Set this keyword to use a "reserved" identifier
;                     between 32 and 49.
;
;       PIXMAP:       Set this keyword to create a pixmap window
;                     (it is actually realized as an unmapped
;                     TopLevelBase).
;
;       NOTOOLBAR:    Set this keyword to prevent adding the toolbar
;                     at the bottom of the window. Note that this
;                     disables interactive capabilities. The window
;                     will act as a regular IDL window with the only
;                     difference being the possibility to resize it.
;                     It can be useful if the toolbar takes too much time to
;                     render (typically when working remotely). The
;                     default is !IGL_D.notoolbar.
;
;
;       BUFFERMAXSIZE: The maximum number of actions recorded for
;                      undo/redo purposes. This is passed as it is to
;                      the LIMIT keyword to the CATTOOLIST::Init
;                      method. The default is 100. (input)
;
;       _REF_EXTRA:    Any keywords accepted by the TOPLEVELBASE::Init method.
;
;-
;*****************************************************************************************************
FUNCTION IGL_Window::Init, WINDOW_ID=window_id, $
                           WINDOW_TITLE=window_title, $                              
                           WINDOW_XSIZE=window_xsize, $
                           WINDOW_YSIZE=window_ysize, $
                           WINDOW_COLOR=window_color, $
                           XPOS=xpos, $
                           YPOS=ypos, $
                           FREE=free, $
                           PIXMAP=pixmap, $
                           NOTOOLBAR=notoolbar, $
                           BUFFERMAXSIZE=buffermaxsize, $
                           _REF_EXTRA=extra

  @cat_func_error_handler

  ;; Initialize global system variables
  IGL_InitSysVars

  ;; Get a unique window identifier between 0 and 49
  windowObjRefs = IGL_GetSysVars('WindowObjRefs') ; Get the window object references
  isOpen = obj_isa_valid(windowObjRefs,'IGL_Window') ; which window is open?
  IF (keyword_set(free)) THEN availableIDs = 32+where(isOpen[32:*] EQ 0,count) $ ; IDs greater or equal than 32 are reserved for "free" windows
  ELSE availableIDs = where(isOpen[0:31] EQ 0,count) ; IDs less than 32 can be specified by the user
  IF (count EQ 0) THEN message, 'Maximum number of windows reached'
  IF (n_elements(window_id) EQ 0 OR keyword_set(free)) THEN window_id = availableIDs[0] $
  ELSE BEGIN
     index = where(window_id EQ availableIDs,count)
     IF (count EQ 0) THEN message, 'Window '+strtrim(window_id,2)+' is already open'
  ENDELSE
  IF (window_id LT 0 OR window_id GT 49) THEN message, 'Window Identifier must be between 0 and 49'

  ;; Default keyword values
  IF (n_elements(window_color) EQ 0) THEN window_color = !IGL_P.background
  IF (n_elements(window_xsize) EQ 0) THEN window_xsize = 640
  IF (n_elements(window_ysize) EQ 0) THEN window_ysize = 480
  IF (n_elements(window_title) EQ 0) THEN window_title = 'IDL '+strtrim(window_id,2)
  IF (n_elements(notoolbar) EQ 0) THEN notoolbar = !IGL_D.notoolbar
  IF (n_elements(xpos) EQ 0) THEN xpos = 0
  IF (n_elements(ypos) EQ 0) THEN ypos = 0
  IF (n_elements(BufferMaxSize) EQ 0) THEN BufferMaxSize = 100
 
  ;; Min/Max dimensions for the draw widget
  screen_size = get_screen_size()
  xmin = 1L
  ymin = 1L
  xmax = screen_size[0]
  ymax = screen_size[1]

  ;; Save device settings and switch to window device
  entryDevice = !d.name
  entryWindow = !d.window
  IF (strupcase(!version.os_family) EQ 'UNIX') THEN set_plot, 'X'
  IF (strupcase(!version.os_family) EQ 'WINDOWS') THEN set_plot, 'WIN'

  ;; Save current color table
  TVLCT, r, g, b, /GET

  ;; Initialize the TOPLEVELBASE superclass
  ok = self -> TopLevelBase::Init(identifier=window_id,$
                                  name='Window',$
                                  description='Graphic window',$
                                  column=1, $
                                  /no_mbar,$
                                  xpad=0,$
                                  ypad=0,$
                                  space=0,$
                                  _extra=extra)

  IF (NOT ok) THEN return, 0

  ;; Create the widgets  
  draw = obj_new('DrawWidget',self,$
                 name='DRAWWIDGET',$
                 /frame,$
                 /erase_window, $
                 tracking_events=1-keyword_set(notoolbar), $ ; Turn on tracking events only if there is a toolbar
                 event_object=self, $
                 event_method='DrawWidgetEventHandler'  $
                )
  
  IF (NOT keyword_set(notoolbar) AND NOT keyword_set(pixmap)) THEN BEGIN
     
     toolbar = obj_new('BaseWidget',self,row=1,xpad=0,ypad=0,space=0)
     
     buttonBase = obj_new('BaseWidget',toolbar,row=1,/toolbar) ; The base for regular buttons
     root_dir = ProgramRootDir(/TWOUP)
     sub_dir = ['resource','bitmaps']
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Save.bmp')
     button = obj_new('ButtonWidget',buttonBase,name='SAVEBUTTON',value=mapfile,/bitmap,tooltip='Save to file')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Undo.bmp')
     button = obj_new('ButtonWidget',buttonBase,name='UNDOBUTTON',value=mapfile,/bitmap,sensitive=0,tooltip='Undo')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Redo.bmp')
     button = obj_new('ButtonWidget',buttonBase,name='REDOBUTTON',value=mapfile,/bitmap,sensitive=0,tooltip='Redo')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_AutoScale.bmp')
     button = obj_new('ButtonWidget',buttonBase,name='AUTOSCALEBUTTON',value=mapfile,/bitmap,tooltip='Auto scale')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Wizard.bmp')
     button = obj_new('ButtonWidget',buttonBase,name='EDITBUTTON',value=mapfile,/bitmap,tooltip='Edit properties')
     
     exclusiveBase = obj_new('BaseWidget',toolbar,row=1,/toolbar,/exclusive) ; The base for exclusive buttons (they trigger an interaction)
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Move.bmp')
     button = obj_new('ButtonWidget',exclusiveBase,name='MOVEBUTTON',value=mapfile,/bitmap,tooltip='Move graphic')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Pan.bmp')
     button = obj_new('ButtonWidget',exclusiveBase,name='PANBUTTON',value=mapfile,/bitmap,tooltip='Pan graphic')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Zoom.bmp')
     button = obj_new('ButtonWidget',exclusiveBase,name='ZOOMBUTTON',value=mapfile,/bitmap,tooltip='Zoom graphic')
     mapfile = filepath(root_dir=root_dir,subdirectory=sub_dir,'IGL_Text.bmp')
     button = obj_new('ButtonWidget',exclusiveBase,name='TEXTBUTTON',value=mapfile,/bitmap,tooltip='Text annotation')
        
     ;;Calculate minimum size allowed for the draw widget based on the toolbar size
     toolbar -> GetProperty, geometry=geometry
     xmin = geometry.xsize
     ymin = geometry.ysize
     
  ENDIF

  ;; Assign an IGL_ColorTable object to the drawWidget to have a private color
  ;; table for the window. Load it with the current color vectors in
  ;; the IGL private color table
  currentTable = IGL_GetSysVars('colorTable')
  currentTable -> GetProperty, ColorPalette=palette
  colorTable = obj_new('IGL_ColorTable')
  colorTable -> SetProperty, ColorPalette=palette ; Load it initially with the current color table
  colorTable -> GetProperty, index=ctindex
  LOADCT, GET_NAMES=tableNames, /silent
  draw -> SetProperty, color_object=colorTable ; Add it to the draw widget
  colorTable -> RegisterForMessage, self, 'COLORTOOL_TABLECHANGE' ; Let the window handle the changes in the color table
  colorTable -> RegisterForMessage, draw, 'COLORTOOL_SETPROPERTY', /unregister

  ;; Set up editable properties
  self -> RegisterProperty, 'ColorTable', 0, name='Color Table', userdef=strlowcase(tableNames[ctindex])
  self -> RegisterProperty, 'window_color', 0, name='Color (background)', userdef=CapFirstLetter(window_color)
  self -> RegisterProperty, 'window_xsize', name='Xsize', /integer, valid_range=[xmin,xmax,1]
  self -> RegisterProperty, 'window_ysize', name='Ysize', /integer, valid_range=[ymin,ymax,1]
  self -> RegisterProperty, 'window_title', name='Title', /string
  self -> SetPropertyAttribute, 'name', sensitive=0
  self -> SetPropertyAttribute, 'description', sensitive=0

  ;; Populate the object
  self.xmin = xmin
  self.ymin = ymin
  self.window_id = window_id
  self -> SetProperty, xoffset=xpos, yoffset=ypos, map=1-keyword_set(pixmap), /size_events
  self -> SetProperty, window_title=window_title, $
                       window_xsize=window_xsize, $
                       window_ysize=window_ysize, $
                       window_color=window_color
  
  ;; Assign a buffer pixmap to the draw widget for buffered graphic
  ;; output unless this window itself is used as a pixmap
  IF (NOT keyword_set(pixmap)) THEN BEGIN
     buffer = obj_new('PIXMAPWIDGET',xsize=window_xsize,ysize=window_ysize,backgroundcolor=window_color)
     draw -> SetProperty, RefreshBuffer=buffer
     buffer -> Add, colorTable
     buffer -> AddParent, self
     self.buffer = buffer
  ENDIF

  ;; Initialize the tool manager 
  toolManager = obj_new('CatToolList',limit=BufferMaxSize)
  IF (NOT obj_valid(toolManager)) THEN message, 'Failed to initialize the tool manager'
  toolManager -> AddParent, self
  self.toolManager = toolManager

  ;; Update system variables
  windowObjRefs = IGL_GetSysVars('WindowObjRefs')
  windowObjRefs[window_id] = self
  IGL_SetSysVars, 'WindowObjRefs', windowObjRefs
  IGL_SetSysVars, 'currentWindow', window_id
  !igl_d.window = window_id
  !igl_d.x_size = window_xsize
  !igl_d.y_size = window_ysize
  !igl_d.win_state[window_id] = 1
  
  ;; Display the entire application in the window.
  self -> TopLevelBase::Draw

  ;; Restore the previous color table
  TVLCT, r, g, b

  ;; Restore saved device and window
  IF windowAvailable(entryWindow) THEN wset, entryWindow ELSE wset, -1
  set_plot, entryDevice

  ;; Report completion
  self -> Report, /Completed
  
  return, 1
  
END


;*****************************************************************************************************
;
; NAME:
;       IGL_WINDOW CLASS DEFINITION
;
; PURPOSE:
;
;       This is the IGL_Window object's structure definition code.
;
;*****************************************************************************************************
PRO IGL_Window__define, class

  COMPILE_OPT idl2, HIDDEN
  
  class = {IGL_Window, $
           INHERITS TopLevelBase, $    ; This is a TOPLEVELBASE widget 
           window_id:0L, $             ; Window index
           window_title:'', $          ; Window title
           xmin:0L, $                  ; Minimum dimensions
           ymin:0L, $                  ; for the draw widget
           sx:0L, $                    ; Record X and Y coordinates
           sy:0L, $                    ; of last cursor position
           selectedObject:obj_new(), $ ; Record selected object for interaction
           propertyPanel:Obj_new(), $  ; the reference to the property editor panel
           toolManager:obj_new(), $    ; The CATTOOLLIST Object used for undo/redo capabilities
           buffer:obj_new(), $         ; The PIXMAPWIDGET object used for buffered graphic output
           lastSavedFile:'' $          ; The full name (path+name) of the last file output
          }
  
  struct_hide, class
  
END
